# vPresent is an immersive presentation creation and display application.
# Copyright (C) 2007-2009 by Priority 5 Holdings, Inc.
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the Free
# Software Foundation; either version 2 of the License, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.

import osg2 as osg


class RenderNode(object):
   def __init__(self, projector, camera):
      self.projector = projector
      self.camera    = camera

class Communicator(object):
   def __init__(self, firstContainerID):
      self.firstContainerID = firstContainerID
      self.connType         = 'Multicast'
      self.connIf           = ''
      self.svcAddr          = ''
      self.width            = 300
      self.height           = 300
      self.hServers         = -1
      self.vServers         = -1
      self.clusterNodes     = {}
      self.window           = None
      self.renderNodes      = []

      # This will be given the correct value in connect().
      self.clientWindow = None

   def setConnectionType(self, type):
      self.connType = type

   def setConnectionInterface(self, interface):
      self.connIf = interface

   def setServiceAddress(self, addr):
      self.svcAddr = addr

   def setHServers(self, count):
      self.hServers = count

   def setVServers(self, count):
      self.vServers = count

   def setSize(self, width, height):
      self.width  = width
      self.height = height

      if self.window is not None:
         self.window.setSize(self.width, self.height)

      self.updateCameras()

   def addNode(self, node, connection):
      self.clusterNodes[node] = connection

   def clearNodes(self):
      self.clusterNodes = {}

   def isReady(self):
      return len(self.clusterNodes.keys()) > 0

   def render(self, renderAction):
      if self.window is not None:
         try:
            self.window.render(renderAction)
         except:
            pass
            #traceback.print_exc()

   def updateCameras(self):
      osg.commitChanges()

      # Sync up each matrix camera with the reference camera.
      if self.clientWindow is not None:
         cwin         = self.clientWindow
         ref_cam      = cwin.getPort(0).getCamera()
         near         = ref_cam.getNear()
         far          = ref_cam.getFar()

         for node in self.renderNodes:
            frustum = node.projector.frustum

            proj_mat = osg.Matrix()
            osg.MatrixFrustum(proj_mat, frustum.left * near,
                              frustum.right * near, frustum.bottom * near,
                              frustum.top * near, near, far)

            node.camera.setNear(near)
            node.camera.setFar(far)
            node.camera.setProjectionMatrix(proj_mat)

   def getViewports(self):
      if self.window is not None:
         return self.window.getPort()
      else:
         return []

   def connect(self, refCam, clientWindow):
      assert self.window is None

      num_nodes = len(self.clusterNodes.keys())

      if num_nodes == 0:
         raise Exception("No nodes defined")

      num_projectors = 0
      for node in self.clusterNodes.keys():
         for pipe in node.getPipes():
            if pipe.hasProjector():
               num_projectors += 1

      if num_projectors == 0:
         raise Exception("No projectors defined")

      # We save these so that they can be used later in setSize().
      self.clientWindow = clientWindow

      self.window = osg.MultiDisplayWindow.create()
      self.window.setConnectionType(self.connType)

      if self.connIf:
         self.window.setConnectionInterface(self.connIf)
      if self.svcAddr:
         self.window.setServiceAddress(self.svcAddr)

      if self.hServers > -1:
         self.window.setHServers(self.hServers)
      else:
         self.window.setHServers(num_projectors)

      if self.vServers > -1:
         self.window.setVServers(self.vServers)

      self.window.setSize(self.width, self.height)
      self.window.setManageClientViewports(False)

      for node, connection in self.clusterNodes.iteritems():
         # Inform the render server that it is no longer in the "configuring"
         # state and should prepare itself for receiving the change list.
         connection.putBool(True)
         connection.flush()
         connection.disconnect()

         self.window.pushToServers(str(node.getName()))

      osg.commitChanges()

      nodes = self.clusterNodes.keys()
      self.clearNodes()

      near = refCam.getNear()
      far  = refCam.getFar()

      vp_width = 1.0 / num_projectors

      client_vp  = clientWindow.getPort(0)
      background = client_vp.getBackground()

      proj_index = 0
      for node in nodes:
         for pipe in node.getPipes():
            if pipe.hasProjector():
               camera = osg.MatrixCamera.create()

               proj = pipe.getProjector()

               rot_mat = osg.Matrix()
               rot_mat.setValue(proj.getRotation().getData())
               camera.setModelviewMatrix(rot_mat)

               self.renderNodes.append(RenderNode(proj, camera))

               vp = osg.Viewport.create()
               vp.setCamera(camera)
               vp.setBackground(background)
               vp.setRoot(client_vp.getRoot())
               vp.setSize(proj_index * vp_width, 0,
                          (proj_index + 1) * vp_width, 1)

               self.window.addPort(vp)

               proj_index += 1

      self.updateCameras()

      osg.commitChanges()

      cl = osg.Thread.getCurrentChangeList()
      cl.fillFromCurrentState(self.firstContainerID)

      # Initialize the window.
      self.window.init()

   def disconnect(self):
      self.window      = None
      self.cientWindow = None
      osg.commitChanges()

   def cancelRender(self):
      '''
      Tells the render servers to go back into (or remain in) the
      configuration state, thus preventing any rendering from occurring.
      After this call, self.clusterNodes is empty, and all open network
      connections to the render servers are closed.
      '''
      for node, connection in self.clusterNodes.iteritems():
         try:
            # Inform the server that it is to remain in the "configuring"
            # state.
            connection.putBool(False)
            connection.flush()
            connection.disconnect()
         except Exception, ex:
            print "NOTICE: Failed to cancel communication with %s: %s" % \
                     (node_addr, str(ex))

      self.clearNodes()
